using System;
using System.Collections.Generic;
using ShadeTree.Binding.Settings;
using ShadeTree.Binding.Validation;
using Action=System.Action;

namespace ShadeTree.Binding.Elements
{
    public abstract class BoundScreenElement<CONTROLTYPE, PROPERTYTYPE>
        : ScreenElement<CONTROLTYPE>, IBoundScreenElement
    {
        private readonly Accessor _accessor;
        private readonly List<Action> _changeActions = new List<Action>();
        private readonly List<Action> _lostFocusActions = new List<Action>();
        private IScreenBinder _binder = new NulloBinder();
        private bool _latched;
        private PROPERTYTYPE _originalValue;
        private object _target;


        public BoundScreenElement(Accessor accessor, CONTROLTYPE control) : base(control)
        {
            _accessor = accessor;
            driver.OnLostFocus(doLostFocus);
            

        }


        protected Accessor accessor
        {
            get { return _accessor; }
        }

        #region IBoundScreenElement Members

        public IScreenBinder Binder
        {
            set { _binder = value; }
        }


        private PROPERTYTYPE getValueFromModel()
        {
            object rawValue = _accessor.GetValue(_target);
            if (rawValue == null)
            {
                return default(PROPERTYTYPE);
            }
            else
            {
                return (PROPERTYTYPE)rawValue;
            }
        }

        public virtual void Bind(object target)
        {
            try
            {
                _target = target;

                _originalValue = getValueFromModel();
                updateControl(_originalValue);
            }
            catch (Exception e)
            {
                string message = string.Format("Unable to bind property " + _accessor.FieldName);
                throw new ApplicationException(message, e);
            }
        }

        public virtual bool ApplyChanges()
        {
            if (!isBound())
            {
                return true;
            }

            _accessor.SetValue(_target, getValueFromControl());

            return true;
        }

        public virtual void Reset()
        {
            updateControl(_originalValue);
            _accessor.SetValue(_target, _originalValue);
        }


        public virtual void Update()
        {
            var newValue = getValueFromModel();
            updateControl(newValue);
        }

        public void RegisterChangeHandler(Action action)
        {
            _changeActions.Add(action);
        }

        public void RegisterLostFocusHandler(Action action)
        {
            _lostFocusActions.Add(action);
        }

        private void doLostFocus()
        {
            _lostFocusActions.ForEach(action => runIfNotLatched(action));

            if (this.validatesOnLostFocus())
            {
                runIfNotLatched(()=>notify());
            }
        }

        public NotificationMessage[] Validate()
        {
            if (_target == null)
            {
                return new NotificationMessage[0];
            }

            return _accessor.Validate(_target);
        }

        public string FieldName
        {
            get { return _accessor.FieldName; }
        }

        public override void Focus()
        {
            _latched = true;
            driver.Focus();
            postFocus();

            _latched = false;
        }

        public void StopBinding()
        {
            _target = null;
            tearDown();
        }

        public virtual void SetDefaults()
        {
            // The latching and explicit call to elementValueChanged() is to make sure
            // elements like TextBox fire change events correctly on this method

            bool changed = false;

            withinLatch(() =>
                        {
                            object lastValue = LastChosenValues.Retrieve(driver);
                            if (lastValue != null)
                            {
                                changed = true;
                                resetControl((PROPERTYTYPE) lastValue);
                            }
                        });

            if (changed && isBound())
            {
                elementValueChanged();
            }
        }

        public void RememberLastChoice()
        {
            _changeActions.Add(storeValue);
        }

        public void RebindAllOnChange()
        {
            _changeActions.Insert(0, () => _binder.UpdateScreen());
        }

        #endregion

        protected abstract PROPERTYTYPE getValueFromControl();
        protected abstract void resetControl(PROPERTYTYPE originalValue);

        protected virtual bool validatesOnLostFocus()
        {
            return true;
        }

        protected void nullOutTheElement()
        {
            _accessor.SetValue(_target, null);
            Update();
        }

        protected void updateControl(PROPERTYTYPE newValue)
        {
            withinLatch(() => resetControl(newValue));
        }

        protected bool isBound()
        {
            return _target != null;
        }

        private bool isLatched()
        {
            return _latched || _binder.IsLatched;
        }

        protected virtual void postFocus()
        {
        }

        protected void runIfNotLatched(Action action)
        {
            if (isLatched())
            {
                return;
            }

            action();
        }

        protected void elementValueChanged()
        {
            if (isLatched())
            {
                return;
            }

            ApplyChanges();

            foreach (Action handler in _changeActions)
            {
                handler();
            }

            if (!isBound())
            {
                return;
            }

            if (IsDirty())
            {
                notify();
            }
        }

        private object _lastValue;
        protected void notify()
        {
            if (_lastValue != null && _lastValue.Equals(getValueFromControl()))
            {
                return;                
            }


            withinLatch(() =>
            {
                _binder.Validate(this);

                try
                {
                    _lastValue = getValueFromModel();

                    if (_lastValue != null)
                    {
                        _binder.OnChange();
                    }
                }
                catch (Exception)
                {
                    // Disregard errors
                }
            });
        }


        protected void withinLatch(Action action)
        {
            if (_latched)
            {
                return;
            }

            try
            {
                _latched = true;
                action();
            }
            finally
            {
                _latched = false;
            }
        }

        protected abstract void tearDown();

        private void storeValue()
        {
            object currentValue = getValueFromControl();
            LastChosenValues.Store(driver, currentValue);
        }

        public bool IsDirty()
        {
            object propertyValue = getValueFromModel();

            if (propertyValue == null)
            {
                if (_originalValue != null)
                {
                    return true;
                }
            }
            else if (!propertyValue.Equals(_originalValue))
            {
                return true;
            }

            return false;
        }


        protected override void postEnable()
        {
            UpdateDisplayState(_target);
        }

        public object GetValue()
        {
            return this.getValueFromModel();
        }
    }
}